// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/app_list/views/app_list_main_view.h"

#include "base/macros.h"
#include "base/memory/scoped_ptr.h"
#include "base/run_loop.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/app_list/app_list_switches.h"
#include "ui/app_list/test/app_list_test_model.h"
#include "ui/app_list/test/app_list_test_view_delegate.h"
#include "ui/app_list/views/app_list_folder_view.h"
#include "ui/app_list/views/app_list_item_view.h"
#include "ui/app_list/views/apps_container_view.h"
#include "ui/app_list/views/apps_grid_view.h"
#include "ui/app_list/views/contents_view.h"
#include "ui/app_list/views/search_box_view.h"
#include "ui/app_list/views/test/apps_grid_view_test_api.h"
#include "ui/events/event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/view_model.h"
#include "ui/views/widget/widget.h"

namespace app_list {
namespace test {

    namespace {

        const int kInitialItems = 2;

        class GridViewVisibleWaiter {
        public:
            explicit GridViewVisibleWaiter(AppsGridView* grid_view)
                : grid_view_(grid_view)
            {
            }
            ~GridViewVisibleWaiter() { }

            void Wait()
            {
                if (grid_view_->visible())
                    return;

                check_timer_.Start(FROM_HERE,
                    base::TimeDelta::FromMilliseconds(50),
                    base::Bind(&GridViewVisibleWaiter::OnTimerCheck,
                        base::Unretained(this)));
                run_loop_.reset(new base::RunLoop);
                run_loop_->Run();
                check_timer_.Stop();
            }

        private:
            void OnTimerCheck()
            {
                if (grid_view_->visible())
                    run_loop_->Quit();
            }

            AppsGridView* grid_view_;
            scoped_ptr<base::RunLoop> run_loop_;
            base::RepeatingTimer check_timer_;

            DISALLOW_COPY_AND_ASSIGN(GridViewVisibleWaiter);
        };

        class AppListMainViewTest : public views::ViewsTestBase {
        public:
            AppListMainViewTest()
                : main_widget_(nullptr)
                , main_view_(nullptr)
                , search_box_widget_(nullptr)
                , search_box_view_(nullptr)
            {
            }

            ~AppListMainViewTest() override { }

            // testing::Test overrides:
            void SetUp() override
            {
                views::ViewsTestBase::SetUp();
                delegate_.reset(new AppListTestViewDelegate);

                // In Ash, the third argument is a container aura::Window, but it is always
                // NULL on Windows, and not needed for tests. It is only used to determine
                // the scale factor for preloading icons.
                main_view_ = new AppListMainView(delegate_.get());
                main_view_->SetPaintToLayer(true);
                main_view_->model()->SetFoldersEnabled(true);
                search_box_view_ = new SearchBoxView(main_view_, delegate_.get());
                main_view_->Init(nullptr, 0, search_box_view_);

                main_widget_ = new views::Widget;
                views::Widget::InitParams main_widget_params = CreateParams(views::Widget::InitParams::TYPE_POPUP);
                main_widget_params.bounds.set_size(main_view_->GetPreferredSize());
                main_widget_->Init(main_widget_params);
                main_widget_->SetContentsView(main_view_);

                search_box_widget_ = new views::Widget;
                views::Widget::InitParams search_box_widget_params = CreateParams(views::Widget::InitParams::TYPE_CONTROL);
                search_box_widget_params.parent = main_widget_->GetNativeView();
                search_box_widget_params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
                search_box_widget_->Init(search_box_widget_params);
                search_box_widget_->SetContentsView(search_box_view_);
            }

            void TearDown() override
            {
                main_widget_->Close();
                views::ViewsTestBase::TearDown();
                delegate_.reset();
            }

            // |point| is in |grid_view|'s coordinates.
            AppListItemView* GetItemViewAtPointInGrid(AppsGridView* grid_view,
                const gfx::Point& point)
            {
                const views::ViewModelT<AppListItemView>* view_model = grid_view->view_model_for_test();
                for (int i = 0; i < view_model->view_size(); ++i) {
                    views::View* view = view_model->view_at(i);
                    if (view->bounds().Contains(point)) {
                        return static_cast<AppListItemView*>(view);
                    }
                }

                return NULL;
            }

            void SimulateClick(views::View* view)
            {
                gfx::Point center = view->GetLocalBounds().CenterPoint();
                view->OnMousePressed(ui::MouseEvent(
                    ui::ET_MOUSE_PRESSED, center, center, ui::EventTimeForNow(),
                    ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON));
                view->OnMouseReleased(ui::MouseEvent(
                    ui::ET_MOUSE_RELEASED, center, center, ui::EventTimeForNow(),
                    ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON));
            }

            // |point| is in |grid_view|'s coordinates.
            AppListItemView* SimulateInitiateDrag(AppsGridView* grid_view,
                AppsGridView::Pointer pointer,
                const gfx::Point& point)
            {
                AppListItemView* view = GetItemViewAtPointInGrid(grid_view, point);
                DCHECK(view);

                gfx::Point translated = gfx::PointAtOffsetFromOrigin(point - view->bounds().origin());
                ui::MouseEvent pressed_event(ui::ET_MOUSE_PRESSED, translated, point,
                    ui::EventTimeForNow(), 0, 0);
                grid_view->InitiateDrag(view, pointer, pressed_event);
                return view;
            }

            // |point| is in |grid_view|'s coordinates.
            void SimulateUpdateDrag(AppsGridView* grid_view,
                AppsGridView::Pointer pointer,
                AppListItemView* drag_view,
                const gfx::Point& point)
            {
                DCHECK(drag_view);
                gfx::Point translated = gfx::PointAtOffsetFromOrigin(point - drag_view->bounds().origin());
                ui::MouseEvent drag_event(ui::ET_MOUSE_DRAGGED, translated, point,
                    ui::EventTimeForNow(), 0, 0);
                grid_view->UpdateDragFromItem(pointer, drag_event);
            }

            ContentsView* GetContentsView() { return main_view_->contents_view(); }

            AppsGridView* RootGridView()
            {
                return GetContentsView()->apps_container_view()->apps_grid_view();
            }

            AppListFolderView* FolderView()
            {
                return GetContentsView()->apps_container_view()->app_list_folder_view();
            }

            AppsGridView* FolderGridView() { return FolderView()->items_grid_view(); }

            const views::ViewModelT<AppListItemView>* RootViewModel()
            {
                return RootGridView()->view_model_for_test();
            }

            const views::ViewModelT<AppListItemView>* FolderViewModel()
            {
                return FolderGridView()->view_model_for_test();
            }

            AppListItemView* CreateAndOpenSingleItemFolder()
            {
                // Prepare single folder with a single item in it.
                AppListFolderItem* folder_item = delegate_->GetTestModel()->CreateSingleItemFolder("single_item_folder",
                    "single");
                EXPECT_EQ(folder_item,
                    delegate_->GetTestModel()->FindFolderItem("single_item_folder"));
                EXPECT_EQ(AppListFolderItem::kItemType, folder_item->GetItemType());

                EXPECT_EQ(1, RootViewModel()->view_size());
                AppListItemView* folder_item_view = static_cast<AppListItemView*>(RootViewModel()->view_at(0));
                EXPECT_EQ(folder_item_view->item(), folder_item);

                // Click on the folder to open it.
                EXPECT_FALSE(FolderView()->visible());
                SimulateClick(folder_item_view);
                base::RunLoop().RunUntilIdle();
                EXPECT_TRUE(FolderView()->visible());

#if defined(OS_WIN)
                AppsGridViewTestApi folder_grid_view_test_api(FolderGridView());
                folder_grid_view_test_api.DisableSynchronousDrag();
#endif
                return folder_item_view;
            }

            AppListItemView* StartDragForReparent(int index_in_folder)
            {
                // Start to drag the item in folder.
                views::View* item_view = FolderViewModel()->view_at(index_in_folder);
                gfx::Point point = item_view->bounds().CenterPoint();
                AppListItemView* dragged = SimulateInitiateDrag(FolderGridView(), AppsGridView::MOUSE, point);
                EXPECT_EQ(item_view, dragged);
                EXPECT_FALSE(RootGridView()->visible());
                EXPECT_TRUE(FolderView()->visible());

                // Drag it to top left corner.
                point = gfx::Point(0, 0);
                // Two update drags needed to actually drag the view. The first changes
                // state and the 2nd one actually moves the view. The 2nd call can be
                // removed when UpdateDrag is fixed.
                SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
                SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
                base::RunLoop().RunUntilIdle();

                // Wait until the folder view is invisible and root grid view shows up.
                GridViewVisibleWaiter(RootGridView()).Wait();
                EXPECT_TRUE(RootGridView()->visible());
                EXPECT_EQ(0, FolderView()->layer()->opacity());

                return dragged;
            }

        protected:
            views::Widget* main_widget_; // Owned by native window.
            AppListMainView* main_view_; // Owned by |main_widget_|.
            scoped_ptr<AppListTestViewDelegate> delegate_;
            views::Widget* search_box_widget_; // Owned by |main_widget_|.
            SearchBoxView* search_box_view_; // Owned by |search_box_widget_|.

        private:
            DISALLOW_COPY_AND_ASSIGN(AppListMainViewTest);
        };

    } // namespace

    // Tests changing the AppListModel when switching profiles.
    TEST_F(AppListMainViewTest, ModelChanged)
    {
        delegate_->GetTestModel()->PopulateApps(kInitialItems);
        EXPECT_EQ(kInitialItems, RootViewModel()->view_size());

        // The model is owned by a profile keyed service, which is never destroyed
        // until after profile switching.
        scoped_ptr<AppListModel> old_model(delegate_->ReleaseTestModel());

        const int kReplacementItems = 5;
        delegate_->ReplaceTestModel(kReplacementItems);
        main_view_->ModelChanged();
        EXPECT_EQ(kReplacementItems, RootViewModel()->view_size());
    }

    // Tests that mouse hovering over an app item highlights it
    TEST_F(AppListMainViewTest, MouseHoverToHighlight)
    {
        delegate_->GetTestModel()->PopulateApps(2);
        main_widget_->Show();

        ui::test::EventGenerator generator(GetContext(),
            main_widget_->GetNativeWindow());
        AppListItemView* item0 = RootViewModel()->view_at(0);
        AppListItemView* item1 = RootViewModel()->view_at(1);

        // If experimental launcher, switch to All Apps page
        if (app_list::switches::IsExperimentalAppListEnabled()) {
            GetContentsView()->SetActiveState(AppListModel::STATE_APPS);
            GetContentsView()->Layout();
        }

        generator.MoveMouseTo(item0->GetBoundsInScreen().CenterPoint());
        EXPECT_TRUE(item0->is_highlighted());
        EXPECT_FALSE(item1->is_highlighted());

        generator.MoveMouseTo(item1->GetBoundsInScreen().CenterPoint());
        EXPECT_FALSE(item0->is_highlighted());
        EXPECT_TRUE(item1->is_highlighted());

        generator.MoveMouseTo(gfx::Point(-1, -1));
        EXPECT_FALSE(item0->is_highlighted());
        EXPECT_FALSE(item1->is_highlighted());
    }

// No touch on desktop Mac. Tracked in http://crbug.com/445520.
#if defined(OS_MACOSX) && !defined(USE_AURA)
#define MAYBE_TapGestureToHighlight DISABLED_TapGestureToHighlight
#else
#define MAYBE_TapGestureToHighlight TapGestureToHighlight
#endif

    // Tests that tap gesture on app item highlights it
    TEST_F(AppListMainViewTest, MAYBE_TapGestureToHighlight)
    {
        delegate_->GetTestModel()->PopulateApps(1);
        main_widget_->Show();

        ui::test::EventGenerator generator(GetContext(),
            main_widget_->GetNativeWindow());
        AppListItemView* item = RootViewModel()->view_at(0);

        // If experimental launcher, switch to All Apps page
        if (app_list::switches::IsExperimentalAppListEnabled()) {
            GetContentsView()->SetActiveState(AppListModel::STATE_APPS);
            GetContentsView()->Layout();
        }

        generator.set_current_location(item->GetBoundsInScreen().CenterPoint());
        generator.PressTouch();
        EXPECT_TRUE(item->is_highlighted());

        generator.ReleaseTouch();
        EXPECT_FALSE(item->is_highlighted());
    }

    // Tests dragging an item out of a single item folder and drop it at the last
    // slot.
    TEST_F(AppListMainViewTest, DragLastItemFromFolderAndDropAtLastSlot)
    {
        AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder();
        const gfx::Rect first_slot_tile = folder_item_view->bounds();

        EXPECT_EQ(1, FolderViewModel()->view_size());

        AppListItemView* dragged = StartDragForReparent(0);

        // Drop it to the slot on the right of first slot.
        gfx::Rect drop_target_tile(first_slot_tile);
        drop_target_tile.Offset(first_slot_tile.width() * 2, 0);
        gfx::Point point = drop_target_tile.CenterPoint();
        SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);

        // Drop it.
        FolderGridView()->EndDrag(false);

        // Folder icon view should be gone and there is only one item view.
        EXPECT_EQ(1, RootViewModel()->view_size());
        EXPECT_EQ(
            AppListItemView::kViewClassName,
            static_cast<views::View*>(RootViewModel()->view_at(0))->GetClassName());

        // The item view should be in slot 1 instead of slot 2 where it is dropped.
        AppsGridViewTestApi root_grid_view_test_api(RootGridView());
        root_grid_view_test_api.LayoutToIdealBounds();
        EXPECT_EQ(first_slot_tile, RootViewModel()->view_at(0)->bounds());

        // Single item folder should be auto removed.
        EXPECT_EQ(NULL,
            delegate_->GetTestModel()->FindFolderItem("single_item_folder"));

        // Ensure keyboard selection works on the root grid view after a reparent.
        // This is a regression test for https://crbug.com/466058.
        ui::KeyEvent key_event(ui::ET_KEY_PRESSED, ui::VKEY_RIGHT, ui::EF_NONE);
        GetContentsView()->apps_container_view()->OnKeyPressed(key_event);

        EXPECT_TRUE(RootGridView()->has_selected_view());
        EXPECT_FALSE(FolderGridView()->has_selected_view());
    }

    // Tests dragging an item out of a single item folder and dropping it onto the
    // page switcher. Regression test for http://crbug.com/415530/.
    TEST_F(AppListMainViewTest, DragReparentItemOntoPageSwitcher)
    {
        // Number of apps to populate. Should provide more than 1 page of apps (6*4 =
        // 24).
        const int kNumApps = 30;

        // Ensure we are on the apps grid view page.
        app_list::ContentsView* contents_view = GetContentsView();
        contents_view->SetActiveState(AppListModel::STATE_APPS);
        contents_view->Layout();

        AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder();
        const gfx::Rect first_slot_tile = folder_item_view->bounds();

        delegate_->GetTestModel()->PopulateApps(kNumApps);

        EXPECT_EQ(1, FolderViewModel()->view_size());
        EXPECT_EQ(kNumApps + 1, RootViewModel()->view_size());

        AppListItemView* dragged = StartDragForReparent(0);

        gfx::Rect grid_view_bounds = RootGridView()->bounds();
        // Drag the reparent item to the page switcher.
        gfx::Point point = gfx::Point(grid_view_bounds.width() / 2,
            grid_view_bounds.bottom() - first_slot_tile.height());
        SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);

        // Drop it.
        FolderGridView()->EndDrag(false);

        // The folder should be destroyed.
        EXPECT_EQ(kNumApps + 1, RootViewModel()->view_size());
        EXPECT_EQ(NULL,
            delegate_->GetTestModel()->FindFolderItem("single_item_folder"));
    }

    // Test that an interrupted drag while reparenting an item from a folder, when
    // canceled via the root grid, correctly forwards the cancelation to the drag
    // ocurring from the folder.
    TEST_F(AppListMainViewTest, MouseDragItemOutOfFolderWithCancel)
    {
        CreateAndOpenSingleItemFolder();
        AppListItemView* dragged = StartDragForReparent(0);

        // Now add an item to the model, not in any folder, e.g., as if by Sync.
        EXPECT_TRUE(RootGridView()->has_dragged_view());
        EXPECT_TRUE(FolderGridView()->has_dragged_view());
        delegate_->GetTestModel()->CreateAndAddItem("Extra");

        // The drag operation should get canceled.
        EXPECT_FALSE(RootGridView()->has_dragged_view());
        EXPECT_FALSE(FolderGridView()->has_dragged_view());

        // Additional mouse move operations should be ignored.
        gfx::Point point(1, 1);
        SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
        EXPECT_FALSE(RootGridView()->has_dragged_view());
        EXPECT_FALSE(FolderGridView()->has_dragged_view());
    }

    // Test that dragging an app out of a single item folder and reparenting it
    // back into its original folder results in a cancelled reparent. This is a
    // regression test for http://crbug.com/429083.
    TEST_F(AppListMainViewTest, ReparentSingleItemOntoSelf)
    {
        // Add a folder with 1 item.
        AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder();
        std::string folder_id = folder_item_view->item()->id();

        // Add another top level app.
        delegate_->GetTestModel()->PopulateApps(1);
        gfx::Point drag_point = folder_item_view->bounds().CenterPoint();

        views::View::ConvertPointToTarget(RootGridView(), FolderGridView(),
            &drag_point);

        AppListItemView* dragged = StartDragForReparent(0);

        // Drag the reparent item back into its folder.
        SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged,
            drag_point);
        FolderGridView()->EndDrag(false);

        // The app list model should remain unchanged.
        EXPECT_EQ(1, FolderViewModel()->view_size());
        EXPECT_EQ(2, RootViewModel()->view_size());
        EXPECT_EQ(folder_id, RootGridView()->GetItemViewAt(0)->item()->id());
        EXPECT_NE(nullptr,
            delegate_->GetTestModel()->FindFolderItem("single_item_folder"));
    }

} // namespace test
} // namespace app_list
